/*
 * Copyright 2002-2018 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.util.xml;

import java.io.ByteArrayInputStream;
import java.io.InputStream;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMResult;
import javax.xml.transform.sax.SAXSource;

import org.junit.Before;
import org.junit.Test;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.w3c.dom.Node;
import org.xml.sax.Attributes;
import org.xml.sax.ContentHandler;
import org.xml.sax.InputSource;
import org.xml.sax.Locator;
import org.xml.sax.XMLReader;
import org.xml.sax.ext.LexicalHandler;
import org.xml.sax.helpers.AttributesImpl;

import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.tests.MockitoUtils;
import org.springframework.tests.MockitoUtils.InvocationArgumentsAdapter;

import static org.junit.Assert.*;
import static org.mockito.BDDMockito.*;

/**
 * @author Arjen Poutsma
 */
public abstract class AbstractStaxXMLReaderTestCase {

    protected static XMLInputFactory inputFactory;

    private XMLReader standardReader;

    private ContentHandler standardContentHandler;


    @Before
    @SuppressWarnings("deprecation")  // on JDK 9
    public void setUp() throws Exception {
        inputFactory = XMLInputFactory.newInstance();
        standardReader = org.xml.sax.helpers.XMLReaderFactory.createXMLReader();
        standardContentHandler = mockContentHandler();
        standardReader.setContentHandler(standardContentHandler);
    }


    @Test
    public void contentHandlerNamespacesNoPrefixes() throws Exception {
        standardReader.setFeature("http://xml.org/sax/features/namespaces", true);
        standardReader.setFeature("http://xml.org/sax/features/namespace-prefixes", false);
        standardReader.parse(new InputSource(createTestInputStream()));

        AbstractStaxXMLReader staxXmlReader = createStaxXmlReader(createTestInputStream());
        ContentHandler contentHandler = mockContentHandler();
        staxXmlReader.setFeature("http://xml.org/sax/features/namespaces", true);
        staxXmlReader.setFeature("http://xml.org/sax/features/namespace-prefixes", false);
        staxXmlReader.setContentHandler(contentHandler);
        staxXmlReader.parse(new InputSource());

        verifyIdenticalInvocations(standardContentHandler, contentHandler);
    }

    @Test
    public void contentHandlerNamespacesPrefixes() throws Exception {
        standardReader.setFeature("http://xml.org/sax/features/namespaces", true);
        standardReader.setFeature("http://xml.org/sax/features/namespace-prefixes", true);
        standardReader.parse(new InputSource(createTestInputStream()));

        AbstractStaxXMLReader staxXmlReader = createStaxXmlReader(createTestInputStream());
        ContentHandler contentHandler = mockContentHandler();
        staxXmlReader.setFeature("http://xml.org/sax/features/namespaces", true);
        staxXmlReader.setFeature("http://xml.org/sax/features/namespace-prefixes", true);
        staxXmlReader.setContentHandler(contentHandler);
        staxXmlReader.parse(new InputSource());

        verifyIdenticalInvocations(standardContentHandler, contentHandler);
    }

    @Test
    public void contentHandlerNoNamespacesPrefixes() throws Exception {
        standardReader.setFeature("http://xml.org/sax/features/namespaces", false);
        standardReader.setFeature("http://xml.org/sax/features/namespace-prefixes", true);
        standardReader.parse(new InputSource(createTestInputStream()));

        AbstractStaxXMLReader staxXmlReader = createStaxXmlReader(createTestInputStream());
        ContentHandler contentHandler = mockContentHandler();
        staxXmlReader.setFeature("http://xml.org/sax/features/namespaces", false);
        staxXmlReader.setFeature("http://xml.org/sax/features/namespace-prefixes", true);
        staxXmlReader.setContentHandler(contentHandler);
        staxXmlReader.parse(new InputSource());

        verifyIdenticalInvocations(standardContentHandler, contentHandler);
    }

    @Test
    public void whitespace() throws Exception {
        String xml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?><test><node1> </node1><node2> Some text </node2></test>";

        Transformer transformer = TransformerFactory.newInstance().newTransformer();

        AbstractStaxXMLReader staxXmlReader = createStaxXmlReader(
                new ByteArrayInputStream(xml.getBytes("UTF-8")));

        SAXSource source = new SAXSource(staxXmlReader, new InputSource());
        DOMResult result = new DOMResult();

        transformer.transform(source, result);

        Node node1 = result.getNode().getFirstChild().getFirstChild();
        assertEquals(" ", node1.getTextContent());
        assertEquals(" Some text ", node1.getNextSibling().getTextContent());
    }

    @Test
    public void lexicalHandler() throws Exception {
        Resource testLexicalHandlerXml = new ClassPathResource("testLexicalHandler.xml", getClass());

        LexicalHandler expectedLexicalHandler = mockLexicalHandler();
        standardReader.setContentHandler(null);
        standardReader.setProperty("http://xml.org/sax/properties/lexical-handler", expectedLexicalHandler);
        standardReader.parse(new InputSource(testLexicalHandlerXml.getInputStream()));
        inputFactory.setProperty("javax.xml.stream.isCoalescing", Boolean.FALSE);
        inputFactory.setProperty("http://java.sun.com/xml/stream/properties/report-cdata-event", Boolean.TRUE);
        inputFactory.setProperty("javax.xml.stream.isReplacingEntityReferences", Boolean.FALSE);
        inputFactory.setProperty("javax.xml.stream.isSupportingExternalEntities", Boolean.FALSE);

        LexicalHandler actualLexicalHandler = mockLexicalHandler();
        willAnswer(invocation -> invocation.getArguments()[0] = "element").
                given(actualLexicalHandler).startDTD(anyString(), anyString(), anyString());
        AbstractStaxXMLReader staxXmlReader = createStaxXmlReader(testLexicalHandlerXml.getInputStream());
        staxXmlReader.setProperty("http://xml.org/sax/properties/lexical-handler", actualLexicalHandler);
        staxXmlReader.parse(new InputSource());

        // TODO: broken comparison since Mockito 2.2 upgrade
        // verifyIdenticalInvocations(expectedLexicalHandler, actualLexicalHandler);
    }


    private LexicalHandler mockLexicalHandler() throws Exception {
        LexicalHandler lexicalHandler = mock(LexicalHandler.class);
        willAnswer(new CopyCharsAnswer()).given(lexicalHandler).comment(any(char[].class), anyInt(), anyInt());
        return lexicalHandler;
    }

    private InputStream createTestInputStream() {
        return getClass().getResourceAsStream("testContentHandler.xml");
    }

    protected final ContentHandler mockContentHandler() throws Exception {
        ContentHandler contentHandler = mock(ContentHandler.class);
        willAnswer(new CopyCharsAnswer()).given(contentHandler).characters(any(char[].class), anyInt(), anyInt());
        willAnswer(new CopyCharsAnswer()).given(contentHandler).ignorableWhitespace(any(char[].class), anyInt(), anyInt());
        willAnswer(new Answer<Object>() {
            @Override
            public Object answer(InvocationOnMock invocation) throws Throwable {
                invocation.getArguments()[3] = new AttributesImpl((Attributes) invocation.getArguments()[3]);
                return null;
            }
        }).given(contentHandler).startElement(anyString(), anyString(), anyString(), any(Attributes.class));
        return contentHandler;
    }

    protected <T> void verifyIdenticalInvocations(T expected, T actual) {
        MockitoUtils.verifySameInvocations(expected, actual,
                new SkipLocatorArgumentsAdapter(), new CharArrayToStringAdapter(), new PartialAttributesAdapter());
    }

    protected abstract AbstractStaxXMLReader createStaxXmlReader(InputStream inputStream) throws XMLStreamException;


    private static class SkipLocatorArgumentsAdapter implements InvocationArgumentsAdapter {

        @Override
        public Object[] adaptArguments(Object[] arguments) {
            for (int i = 0; i < arguments.length; i++) {
                if (arguments[i] instanceof Locator) {
                    arguments[i] = null;
                }
            }
            return arguments;
        }
    }


    private static class CharArrayToStringAdapter implements InvocationArgumentsAdapter {

        @Override
        public Object[] adaptArguments(Object[] arguments) {
            if (arguments.length == 3 && arguments[0] instanceof char[]
                    && arguments[1] instanceof Integer && arguments[2] instanceof Integer) {
                return new Object[]{new String((char[]) arguments[0], (Integer) arguments[1], (Integer) arguments[2])};
            }
            return arguments;
        }
    }


    private static class PartialAttributesAdapter implements InvocationArgumentsAdapter {

        @Override
        public Object[] adaptArguments(Object[] arguments) {
            for (int i = 0; i < arguments.length; i++) {
                if (arguments[i] instanceof Attributes) {
                    arguments[i] = new PartialAttributes((Attributes) arguments[i]);
                }
            }
            return arguments;
        }
    }


    private static class CopyCharsAnswer implements Answer<Object> {

        @Override
        public Object answer(InvocationOnMock invocation) throws Throwable {
            char[] chars = (char[]) invocation.getArguments()[0];
            char[] copy = new char[chars.length];
            System.arraycopy(chars, 0, copy, 0, chars.length);
            invocation.getArguments()[0] = copy;
            return null;
        }
    }


    private static class PartialAttributes {

        private final Attributes attributes;

        public PartialAttributes(Attributes attributes) {
            this.attributes = attributes;
        }

        @Override
        public boolean equals(Object obj) {
            Attributes other = ((PartialAttributes) obj).attributes;
            if (this.attributes.getLength() != other.getLength()) {
                return false;
            }
            for (int i = 0; i < other.getLength(); i++) {
                boolean found = false;
                for (int j = 0; j < attributes.getLength(); j++) {
                    if (other.getURI(i).equals(attributes.getURI(j))
                            && other.getQName(i).equals(attributes.getQName(j))
                            && other.getType(i).equals(attributes.getType(j))
                            && other.getValue(i).equals(attributes.getValue(j))) {
                        found = true;
                        break;
                    }
                }
                if (!found) {
                    return false;
                }
            }
            return true;
        }

        @Override
        public int hashCode() {
            return 1;
        }
    }

}
